﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;

namespace Safebrowsing
{
    /// <summary>
    /// Maintenance worker. Issues requests against 
    /// </summary>
    internal class MaintenanceWorker : IDisposable
    {
        private readonly AutoResetEvent m_StopEvent = new AutoResetEvent(false);
        private readonly object m_SyncRoot = new object();
        private readonly MaintenancePolicy m_MaintenancePolicy;
        private readonly ErrorState m_ErrorState;
        private readonly SafebrowsingDao m_SafebrowsingDao;
        private readonly SafebrowsingApiServicePoint m_DefaultApiServicePoint;
        private Thread m_WorkerThread;
        private bool m_IsStarted;

        public event EventHandler<SafebrowsingListUpdatedEventArgs> SafebrowsingListUpdated;

        /// <summary>
        /// Gets or sets proxy to use for downloading lists
        /// </summary>
        public IWebProxy Proxy { get; set; }

        /// <summary>
        /// Collection of safebrowsing lists this worker updates
        /// </summary>
        public List<SafebrowsingList> Lists { get; private set; }

        /// <summary>
        /// Creates an instance of the maintenance worker
        /// </summary>
        /// <param name="maintenancePolicy"></param>
        /// <param name="safebrowsingDao"></param>
        /// <param name="defaultServicePoint"></param>
        /// <param name="lists">Collection of lists this worker should update</param>
        internal MaintenanceWorker(MaintenancePolicy maintenancePolicy, SafebrowsingDao safebrowsingDao, 
            SafebrowsingApiServicePoint defaultServicePoint, IEnumerable<SafebrowsingList> lists)
        {
            if (maintenancePolicy == null) throw new ArgumentNullException("maintenancePolicy");
            if (safebrowsingDao == null) throw new ArgumentNullException("safebrowsingDao");
            if (defaultServicePoint == null) throw new ArgumentNullException("defaultServicePoint");
            if (lists == null) throw new ArgumentNullException("lists");

            m_ErrorState = new ErrorState();
            Lists = lists is List<SafebrowsingList> ? (List<SafebrowsingList>) lists : new List<SafebrowsingList>(lists);
            m_MaintenancePolicy = maintenancePolicy;
            m_SafebrowsingDao = safebrowsingDao;
            m_DefaultApiServicePoint = defaultServicePoint;
        }

        /// <summary>
        /// Called when safebrowsing list has been updated
        /// </summary>
        /// <param name="list"></param>
        protected void OnSafebrowsingListUpdated(SafebrowsingList list)
        {
            var safebrowsingListUpdated = SafebrowsingListUpdated;
            if (safebrowsingListUpdated == null) return;
            safebrowsingListUpdated(this, new SafebrowsingListUpdatedEventArgs(list));
        }

        private void DoWorkLoop()
        {
            // Waiting for the first request delay
            m_StopEvent.WaitOne(m_MaintenancePolicy.FirstRequestDelay, false);

            while (m_IsStarted)
            {
                int nextRequestInterval = 0;
                
                if (Lists.Count > 0)
                {
                    try
                    {
                        // Doing some maintenance work
                        var dataRequests = CreateDataRequests();
                        bool reset = false;

                        foreach (KeyValuePair<SafebrowsingApiServicePoint, DataRequest> pair in dataRequests)
                        {
                            try
                            {
                                var apiClient = new SafebrowsingApiClient(pair.Key) { Proxy = Proxy };

                                DataResponse dataResponse = apiClient.RequestData(pair.Value);
                                if (dataResponse.MinDelay > nextRequestInterval) nextRequestInterval = dataResponse.MinDelay;
                                if (dataResponse.Reset)
                                {
                                    reset = true;
                                    break;
                                }

                                foreach (DataListResponse dataListResponse in dataResponse.DataLists)
                                {
                                    var redirectResponses = new List<RedirectResponse>();

                                    foreach (Uri redirectUri in dataListResponse.RedirectUrls)
                                    {
                                        redirectResponses.Add(apiClient.RequestRedirectData(redirectUri));
                                    }

                                    // Checking worker status one more time
                                    if (!m_IsStarted) return;

                                    // Saving response
                                    m_SafebrowsingDao.SaveApiResponse(dataListResponse, redirectResponses);

                                    // Calling updated event handler
                                    OnSafebrowsingListUpdated(dataListResponse.List);
                                }
                            }
                            catch (Exception ex)
                            {
                                Logger.Error("Error doing maintenance: {0}", ex);
                                m_ErrorState.ErrorsCount++;
                                m_ErrorState.LastErrorTime = DateTime.Now;
                            }
                        }

                        if (reset)
                        {
                            // Reseting database and doing data request again
                            m_SafebrowsingDao.Reset();
                            continue;
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Error("Fatal error during maintenance: {0}", ex);
                        m_ErrorState.ErrorsCount++;
                        m_ErrorState.LastErrorTime = DateTime.Now;
                    }
                }

                // Waiting for the specified period of time
                m_StopEvent.WaitOne(nextRequestInterval == 0 ? GetWaitPeriod() : new TimeSpan(0, 0, nextRequestInterval), false);
            }
        }

        /// <summary>
        /// Creates collection of pairs of service point and data request.
        /// </summary>
        /// <returns></returns>
        private Dictionary<SafebrowsingApiServicePoint, DataRequest> CreateDataRequests()
        {
            var dataRequests = new Dictionary<SafebrowsingApiServicePoint, DataRequest>();

            foreach (SafebrowsingList list in Lists)
            {
                List<int> addChunksNumbers = m_SafebrowsingDao.GetAddChunksNumbers(list);
                List<int> subChunksNumbers = m_SafebrowsingDao.GetSubChunksNumbers(list);
                SafebrowsingApiServicePoint servicePoint = list.ApiServicePoint ?? m_DefaultApiServicePoint;

                if (!dataRequests.ContainsKey(servicePoint))
                {
                    dataRequests.Add(servicePoint, new DataRequest());
                }

                dataRequests[servicePoint].AddDataListRequest(list, addChunksNumbers, subChunksNumbers);
            }

            return dataRequests;
        }

        private TimeSpan GetWaitPeriod()
        {
            if (m_ErrorState.ErrorsCount == 0) return m_MaintenancePolicy.DefaultRequestPeriod;

            if (m_MaintenancePolicy.ErrorIntervals.Length < m_ErrorState.ErrorsCount)
            {
                // returning last interval from the policy
                return m_MaintenancePolicy.ErrorIntervals[m_MaintenancePolicy.ErrorIntervals.Length - 1];
            }
            
            // returning interval for this error count
            return m_MaintenancePolicy.ErrorIntervals[m_ErrorState.ErrorsCount - 1];
        }

        /// <summary>
        /// Starts maintenenace worker
        /// </summary>
        public void Start()
        {
            lock (m_SyncRoot)
            {
                if (m_IsStarted) return;
                m_IsStarted = true;
                m_WorkerThread = new Thread(DoWorkLoop) { IsBackground = true };
                m_WorkerThread.Start();
            }
        }

        /// <summary>
        /// Stops maintenance worker
        /// </summary>
        public void Stop()
        {
            lock (m_SyncRoot)
            {
                if (!m_IsStarted) return;
                m_IsStarted = false;
                m_StopEvent.Set();
                m_WorkerThread.Join();
            }
        }

        /// <summary>
        /// Returns "true" if maintenance worker is started
        /// </summary>
        public bool IsStarted
        {
            get
            {
                lock (m_SyncRoot)
                {
                    return m_IsStarted;                    
                }
            }
        }

        /// <summary>
        /// Disposes maintenance worker
        /// </summary>
        public void Dispose()
        {
            Stop();
            m_StopEvent.Close();
        }
    }
}
